package cn.zyq.charging.cost.service.impl;

import cn.zyq.charging.common.pojo.param.ChargingProcessParam;
import cn.zyq.charging.common.pojo.vo.ChargingProcessVO;
import cn.zyq.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.zyq.charging.cost.dao.repository.CostRuleRepository;
import cn.zyq.charging.cost.pojo.ChargingData;
import cn.zyq.charging.cost.pojo.po.CostRulePO;
import cn.zyq.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

//1.用户手机扫码调用订单服务生成订单
//2.订单服务调用设备服务更新充电枪状态。
//3.订单服务然后给设备发送充电指令。
//4.设备执行充电指令，并给订单服务返回充电成功或失败的结果，
//5.订单服务给mysql插入一条订单数据(给charging_bill_success表中插入执行充电指令成功的数据，或者给charging_bill_fail表中插入执行充电指令失败的数据)。
//6.设备间隔给订单服务同步充电数据。
//7.订单服务调用计价服务计算充电费用。  //计价//计价//计价//计价//计价
//8.计价服务返回充电费用给订单服务，订单服务再返回充电费用给用户。  //计价//计价//计价//计价//计价
//9.
@Slf4j
@Service
public class CostServiceImpl implements CostService {

    /**
     * Map集合中存储了每个订单的充电信息，每个订单对应一个ChargingData对象(key是订单号 value是充电数据)，
     * 每次电量更新都会修改Map集合中key对应的value(ChargingData对象)信息，
     * (下一次充电的总度数是上一次充电的电量加上本次的充电量)
     */
    private Map<String, ChargingData> chargingProcessData = new HashMap<>();
    //后续会将这个Map数据存入到ES中(删除这个Map集合)(避免计价服务宕机或多个计价服务数据一致性的问题)。

    /**
     * 1.计算充电费用
     * 1.什么样的因素会影响价格
     *   1.1. 时间段不同价格不同：  晚上便宜 低峰 白天贵 高峰
     *   1.2. 地理位置：  充电站站不同有可能价格不同(有些地区或运营商会偏贵)
     *   1.3. 枪的类型：  快充就贵，  慢充就便宜
     *   1.4. 充电量
     * 2. 设备多次同步充电进度,多次怎么算?
     *   2.1 取最后一次 怎么确定是最后一次 ?
     *     2.1.1 判断是否是第一次或者是第N次
     *     次数            充电度数 价格
     *     第1次同步充电数据  10     20
     *     第2次同步充电数据  20     40 (如果第二次的20度电包含了第一次的10度，第2次同步充电数据时，充电的总度数就是20)
     *     第3次同步充电数据  29     58 (如果第三次的29度电包含了第二次的20度，第3次同步充电数据时，充电的总度数就是29)
     *     第4次同步充电数据  40     80 (如果第四次的40度电包含了第三次的29度，第4次同步充电数据时，充电的总度数就是40)
     *     则总共充了40度电(最后一次同步的充电度数就是充电的总度数， 因为最后一次同步的度数包含了前三次所有的充电度数<不同和前边三次累加>)。
     *
     *     注意： 反过来，如果每次只是同步上一次到本次同步数据之间的时间段内的充电度数，则第一次同步时冲了10度电，第二次同步时冲了20度电，
     *           第三次同步时冲了29度电，第四次同步时冲了40度电， 这四次总共充了99度电。
     *   思考 如果你是设备的设计者,  第2次同步的充电度数  是否应该包含 第1次的充电度数 ?
     *    答案： 包含或者不包含都可以，但是每次同步充电数据都不包含上一次的充电度数就太复杂了 代码实现困难太大(必须保证Mqtt消息不丢失)，
     *          假如丢失一次就损失运营成本了<会造成经济损失>。    所以每次同步充电数据都包含上一次充电的总度数，更容易实现一些。
     * @param chargingProcessParam 封装充电进度的对象。
     * @return 返回封装充电进度的vo对象。
     */
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {
        //1.从Map集合中获取chargingData，如果获取不到就创建一个chargingData放入到Map集合。
        ChargingData chargingData = buildChargingData(chargingProcessParam);

        //2.从参数chargingProcessParam中获取充电站id，并计算当前时间所处的小时数。
        Integer stationId = chargingProcessParam.getStationId();//获取充电站id
        log.debug("CostServiceImpl--calculateCost--获取计价规则:入参:{}",stationId);
        LocalDateTime now = LocalDateTime.now();
        int hour = now.getHour();
        //3.根据当前同步充电数据的小时数，判断目前处于哪个计价规则，得到对应的计价规则后获取计价规则中的单价，然后将单价设置给Map中对应的chargingData中。
        // 注意：将小时数匹配计价规则的代码抽取到getMatchCostRuleByTime()方法中可以复用，还有如果以后匹配计价规则代码发生改变就只需要去getMatchCostRuleByTime()中修改即可。
        CostRulePO costRulePO = getMatchCostRuleByTime(stationId, hour);
        log.debug("通过场站信息:{},充电同步时间:{},获取计价规则{}",stationId,hour,costRulePO);
        if(costRulePO!=null){
            String ruleName= costRulePO.getName();//计价规则名称
            BigDecimal powerFee= costRulePO.getPowerFee();//从计价规则中获取单位电价。
            chargingData.setCurrentOneFee(powerFee);//给充电记录数据设置电费单价
            log.debug("当前时间(小时):{},业务时间段名称(计价规则名称):{},电费单价:{}",hour,ruleName,powerFee);
            //如果是第一次同步充电数据，则给Map中对应的充电记录数据对象chargingData中的上一次充电信息为当前充电信息， 同时给chargingData设置充电的总花费
            if(chargingData.getLastChargingProcessParam()==null){
                //getFirstCost(chargingProcessParam, powerFee, hour, RuleName, chargingData);
                //将上边方法参数顺序调整一下(基本类型的参数往后写)
                appendFirstCostToChargingData(chargingProcessParam, chargingData, powerFee, hour, ruleName);
            }else{
                appendLastCostToChargingData(chargingProcessParam, chargingData, powerFee, hour, ruleName);
            }
        }else{
            //如果没有获取到计价规则,需要告警,接入监控系统,停止充电了.返回空对象
            //如果计价服务找不到计价规则 计价服务返回 null 计价服务告警,订单服务需要对计价服务返回的结果进行判断
            //如果 为 null 表示计算价格失败 订单需要通知设备停止充电
            log.error("获取计价规则失败,告警告警告警");
            return null;
        }

        chargingData.setTotalCapacity(chargingProcessParam.getChargingCapacity());//将最新的充电总度数设置给chargingData
        //注意：chargingData.setTotalCapacity()如果写在initChargingData()方法中(而这里不写)，就会有bug，导致非第一次同步充电数据时充电
        // 总费用会计算出来并更新到ChargingData中，但充电总度数不会更新(因为initChargingData()方法只会在第一次同步充电数据的时候执行，
        // 第一次以后不执行，所以如果不是第一次则同步充电数据时不会用chargingData.setTotalCapacity()更新充电总度数到chargingData)

        //订单编号和充电枪id在buildChargingData(chargingProcessParam)中已经有了，所以设置订单id和充电枪id的两行代码 不用写了

        //用buildChargingProcessVO()将chargingData中的数据封装在ChargingProcessVO对象中返回给CostController，CostController返回给
        //Order服务的MqttConsumer中的handleChargingProcess()中的getOrderCost()
        return buildChargingProcessVO(chargingData, chargingProcessParam);
    }



    /**
     * 1.1.根据订单编号orderNo从Map集合中获取充电记录数据对象chargingData
     * (如果Map集合中有此对象就返回，没有chargingData,则初始化一个chargingData对象存入到Map集合中,并返回chargingData)
     * @param chargingProcessParam chargingProcessParam
     * @return chargingData
     */
    private ChargingData buildChargingData(ChargingProcessParam chargingProcessParam) {
        String orderNo = chargingProcessParam.getOrderNo();//获取订单号
        ChargingData chargingData = chargingProcessData.get(orderNo);//根据订单号从Map集合中获取chargingData
        if(chargingData==null) {//用于判断是否同步充电进度数据了
            chargingData = initChargingData(chargingProcessParam);
            return chargingData;
        }else {
            return chargingData;
        }
    }




    /**
     * 1.1.1.本方法是给上边方法的if分支调用的(其他地方没用到本方法)
     * 创建一个chargingData充电数据对象初始化后(初始化设备和订单的一些基础信息<动态信息比如充电度数温度不能在这里初始化，因为此方法只执行一次>)存入到Map集合中,
     * 并返回此充电数据对象chargingData。
     * 初始化充电记录数据(ChargingData对象，用来标识是否是第一次充电，如果是第一次充电就给Map中存入一个ChargingData对象，
     * 如果不是第一次则Map集合中对于这条订单就会对应一条ChargingData对象)：
     * @param chargingProcessParam 最后一条充电进度数据
     * @return  充电记录数据对象ChargingData
     */
    private ChargingData initChargingData(ChargingProcessParam chargingProcessParam) {
        String orderNo = chargingProcessParam.getOrderNo();//从充电进度数据对象中获取订单编号orderNo
        ChargingData  chargingData = new ChargingData();
        log.debug("通过订单号:{}，从Map集合中没有获取到充电数据，表示本次是第一次同步充电数据", orderNo);
        //创建一个chargingData充电数据对象初始化存入到Map集合中, 并返回此充电数据对象chargingData。
        chargingData.setOrderNo(orderNo);//设置订单号
        //BigDecimal cost = getCost(chargingProcessParam);//获取本次的充电度数
        //把当前当前的充电度数(也是充电总金额)设置到chargingData中
        //设置开始充电时间。//设置实际充电开始时间和订单创建时间是有点小区别的
        chargingData.setStartTime(LocalDateTime.now());
        chargingData.setGunId(chargingProcessParam.getGunId());//设置充电枪id
        chargingData.setUserId(chargingProcessParam.getUserId());//设置用户id(本条数据属于哪个用户)
        //本订单的充电数据对象chargingData保存到Map集合中。
        chargingProcessData.put(orderNo, chargingData);
        return chargingData;
    }




    /**
     * 1.2.此方法只是是给上边的calculateCost()方法调用的(其他地方没调用此方法)
     * 根据当前同步充电数据的时间，判断属于哪个计价规则。
     * @return 计价规则对象CostRulePO
     */
    private CostRulePO getMatchCostRuleByTime(Integer stationId, int hour){
        log.debug("CostServiceImpl-getMatchCostRuleByTime-获取计价规则:入参:{}",stationId);
        //通过充电站站id去mysql数据库获取所有计价规则列表
        List<CostRulePO> costRules=getCostRule(stationId);
        log.debug("获取计价规则:入参:{},出参:{}",stationId,costRules);
        log.debug("获取当前时间 小时 :{} 通过当前小时 来计算 当前小时 属于 哪个时间段 尖 峰 平 谷 计算价格",hour);
        for(CostRulePO costRulePO:costRules){
            //判断当前小时属于哪个时间段
            //if(当前时间 大于等于计价规则的开始时间   &&  小于 结束时间)
            if(costRulePO.getStartTime() <= hour  && hour< costRulePO.getEndTime()){//判断同步的充电进度数据是否是第一次的数据。
                return costRulePO;
            }
        }
        return null;
    }





    @Autowired(required = false)
    private CostRuleRepository costRuleRepository;

    @Autowired(required = false)
    private CostRuleCacheRepository costRuleCacheRepository;
    /**
     * 1.2.1.通过充电站站id去mysql数据库获取计价列表
     * 设备会不停的给订单同步充电进行数据，则计价服务需要不断的通过充电站站id去mysql数据库获取计价列表
     * (这样做如果充电桩特别多并且每个都会不停的同步数据，这样太耗时也会导致mmysql并发量太大。
     * 所以后边把这里改成从redis中获取计价规则了<把mysql中的计价规则从mysql加载到redis中缓存了>)
     * @param stationId  充电站id
     * @return 根据充电站id获取多个计价规则
     * (CostController中的计价方法calculateCost()调用了getMatchCostRuleByTime()，getMatchCostRuleByTime()调用了getCostRule()，getCostRule()中调用方法getCostRuleByStationId()):
     */
    private List<CostRulePO> getCostRule(Integer stationId) {
        log.debug("通过站点id:{}从redis获取计价规则",stationId);
        //1.先从Redis中获取计价规则的数据
        List<CostRulePO> cacheCostRules=costRuleCacheRepository.getCostRuleByStationId(stationId);
        //每次设备同步充电进度数据给服务端时，不能直接从mysql读取计价规则(因为效率慢)。
        //  应该每次设备同步充电进度数据给服务端时，先从redis读取计价规则如果有直接使用，如果没有再从mysql查询计价规则再存到redis中最后使用。
        //2.判断redis是否有数据
        //  2.1.有数据就返回从redis查询的数据。
        if(!CollectionUtils.isEmpty(cacheCostRules)){
            log.debug("通过站点id:{}从redis获取计价规则,redis有数据 直接返回",stationId);
            return cacheCostRules;
        }else{
            log.debug("通过站点id:{}从 Redis没有查询到数据 从数据库查询",stationId);
            //2.2.如果Redis没有计价规则数据，则从mysql数据库中获取计价规则的数据
            List<CostRulePO> dbCostRules = costRuleRepository.getCostRuleByStationId(stationId);
            //2.2.1.判断数据库是否有数据
            if(!CollectionUtils.isEmpty(dbCostRules)){
                log.debug("通过站点id:{} 数据库有{}条数据 写入到Redis,并返回",stationId,dbCostRules.size());
                //A.数据库有数据，就将从数据库查询出的数据dbCostRules，存入到redis中
                //这种方式采用的是懒汉式，当实际需要计价规则数据时才从mysql中查询数据放并缓存到redis中。
                costRuleCacheRepository.saveCostRule(stationId,dbCostRules);
                return dbCostRules;
            }/*else{
                return Collections.emptyList();//否则就返回空集合

            }*/
        }
        return Collections.emptyList();//否则就返回空集合//被注释掉的return和这个return作用一样。
    }





    /** appendFirstCostToChargingData()和appendLastCostToChargingData()两个方法获取充电度数的代码不同。 计算总花费方式也不同
     * (一个是本次充电费用就是总花费，另一个是之前的充电总费用 + 本次的花费)
     * 1.3.计算的第一次的充电价格并设置到ChargingData中： 不需要累加每次的充电费用。
     * @param chargingProcessParam 表示本次同步的充电数据，用来获取充电的度数， 同时用于充当上一次的充电数据。
     * @param powerFee 单位电价，用来和chargingProcessParam获取的充电度数相乘。
     * @param hour 用于log.debug()打印。
     * @param RuleName 用于log.debug()打印。
     * @param chargingData 用于保存充电总费用和上一次充电数据。
     */
    private void appendFirstCostToChargingData(ChargingProcessParam chargingProcessParam,ChargingData chargingData, BigDecimal powerFee, int hour, String RuleName) {
        log.debug("通过订单号orderNo从充电记录对象chargingData中获取上一次同步的充电数据， 如果获取不到就表示是第一次同步充电数据11");
        //获取充电度数。
        //1.获取本次同步的充电度数(这个if分支表示第一次同步充电数据，所以这个充电度数就是本次实际的充电度数)
        float chargingCapacity = chargingProcessParam.getChargingCapacity();
        //单价powerFee  * 度数chargingCapacityBigDecimal = 本次充电费用
        //但是有个问题：充电时间比较长，有可能会跨越多个计价规则，每个计价规则单位电价不同，所以计算本次充电总费用计算稿方式需要调整成下边格式：
        //平时间段的时长  * 平时间段的电价   +     尖下午时间段的时长  * 尖下午时间段的电价 +   峰时间段的时长  * 峰时间段的电价  = 本次充电费用
        //2.用getCost()计算本次的充电费用chargingCost ： 单位电价 * 本次充电度数
        //注意： 因为这个if表示第一次同步充电数据，所以本次充电费用也就是目前充电的总费用。
        BigDecimal chargingCost = getCost(chargingCapacity, powerFee);
        log.debug("当前时间(小时){}，计价规则名称{},电费单价:{},充电度数:{},本次花费总费用:{}", hour, RuleName, powerFee,chargingCapacity,chargingCost);
        //3.给ChargingData中存入总花费： 本次同步充电信息的总花费就是充电的总花费(包含之前的总花费)
        chargingData.setTotalCost(chargingCost);//给充电记录中保存目前的充电总费用。
        //4.将本次同步的充电数据对象chargingProcessParam存入到chargingData(作为上一次的充电的数据)
        chargingData.setLastChargingProcessParam(chargingProcessParam);//给充电记录中保存最后一次同步的充电进度数据。
    }



    /**
     * appendFirstCostToChargingData()和appendLastCostToChargingData()两个方法获取充电度数的代码不同。 计算总花费方式也不同
     * (一个是本次充电费用就是总花费，另一个是之前的充电总费用 + 本次的花费)
     * 1.4.计算的不是第一次的充电价格并设置到ChargingData中： 需要累加每次的充电费用。
     * @param chargingProcessParam 充电进度数据
     * @param chargingData 充电记录对象
     * @param powerFee 单位电价
     * @param hour 当前同步充电数据的小时数
     * @param RuleName 计价规则。
     */
    private void appendLastCostToChargingData(ChargingProcessParam chargingProcessParam, ChargingData chargingData, BigDecimal powerFee, int hour, String RuleName) {
        //上边的一段代码优化到了getRealChargingCapacity()方法中了。//替换成下边的一行代码了
        //1.计算本次实际的充电度数(上一次同步和本次同步之间实际的充电度数)。
        float realChargingCapacity=getRealChargingCapacity(chargingProcessParam, chargingData);

        //2.用getCost()计算本次的充电费用chargingCost ： 单位电价 * 本次充电度数
        BigDecimal currentChargingCost = getCost(realChargingCapacity, powerFee);

        log.debug("当前时间(小时){}，计价规则名称{},电费单价:{},本次充电度数:{},本次花费总费用:{}", hour, RuleName, powerFee,realChargingCapacity,currentChargingCost);

        //3.用本次的充电费用currentChargingCost和之前的充电总费用累加计算目前充电的总费用。
        BigDecimal totalCost = chargingData.getTotalCost();//获取上次的花费
        BigDecimal currentTotalCost=totalCost.add(currentChargingCost);//上次的总花费 + 本次花费
        //4.给ChargingData中存入目前的总花费： 本次同步充电信息的总花费就是充电的总花费(包含之前的总花费)
        chargingData.setTotalCost(currentTotalCost);
        //5.将本次同步的充电数据对象chargingProcessParam存入到chargingData(作为上一次的充电的数据)
        //给ChargingData中存入本次同步的充电进度数据（为下一次同步充电进度数据 计算充电费用时做准备）。
        chargingData.setLastChargingProcessParam(chargingProcessParam);
    }


    /**
     * 1.4.1.计算上一次同步到本次同步数据之间，实际的充电度数:
     * @param chargingProcessParam (本次同步的充电数据<包含所有次同步的充电度数>) 当前同步数据的入参，包含当前同步充电的度数
     * @param chargingData (上次同步的充电数据)上传同步数据的入参，包含上次同步充电的数据的对象chargingProcessParam
     * @return  上次同步到本次同步之间充电度数=包含所有次同步的充电度数 - 上次同步的充电数据
     */
    private float getRealChargingCapacity(ChargingProcessParam chargingProcessParam,ChargingData chargingData){
        ChargingProcessParam lastChargingProcessParam = chargingData.getLastChargingProcessParam();//上一次的充电度数
        float lastChargingCapacity= lastChargingProcessParam.getChargingCapacity();//获取上一次的充电度数
        float currentChargingCapacity = chargingProcessParam.getChargingCapacity();//全部的充电度数
        //计算实际的充电度数   : 本次同步的全部充电度数 - 上一次同步的充电度数
        if(lastChargingProcessParam==null){
            return currentChargingCapacity;
        }else {
            return currentChargingCapacity - lastChargingCapacity;//本次同步的全部充电度数 - 上一次同步的充电度数
        }
    }

    /**
     * 1.4.2.从chargingProcessParam中获取本次充电的度数: 将float类型转化为BigDecimal类型。
     * 计算总费用： 单价*充电总度数
     * @param chargingCapacity 充电的度数
     * @param powerFee 单价
     * @return  总花费=单价*总度数
     * 注意：将计算总充电费用的代码封装到getCost()方法中，后边如果要计算总充电费用时只需要在getCost方法中添加即可
     * (不需要在calculateCost()方法里找到对应地方添加)。
     */
    private BigDecimal getCost(float chargingCapacity, BigDecimal powerFee){
        //Float.toString()：将 float 数据转换为字符串格式，避免直接使用构造函数传入 float 值导致精度问题。
        //new BigDecimal(String)：通过字符串创建 BigDecimal 对象，确保数值的精确性。
        //运算操作：可以通过 multiply 方法进行乘法计算，得到最终费用。
        String floatStr = Float.toString(chargingCapacity);
        //将float类型的充电总度数转化为BigDecimal类型。
        BigDecimal chargingCapacityBD = new BigDecimal(floatStr);

        return powerFee.multiply(chargingCapacityBD);//return 总花费=单价*总度数
    }





    /**
     * 1.5.构建一个chargingProcessVO
     * (业务层给控制层返回充电进度结果信息的值对象，里边包含：充电总时长，充电总度数，本次同步充电数据时的单位电价，充电总费用，订单id，充电枪id)
     * @param chargingData Map中的充电记录数据(用户的一次充电消费对应Map集合中的一个chargingData对象<key是订单id>)
     * @param chargingProcessParam 本次同步的充电数据
     * @return ChargingProcessVO 充电进度值对象
     */
    private ChargingProcessVO buildChargingProcessVO(ChargingData  chargingData,ChargingProcessParam chargingProcessParam) {
        ChargingProcessVO chargingProcessVO=new ChargingProcessVO();
        chargingProcessVO.setTotalCost(chargingData.getTotalCost());//总花费
        chargingProcessVO.setOrderNo(chargingData.getOrderNo());//订单编号
        chargingProcessVO.setUserId(chargingData.getUserId());//用户id
        chargingProcessVO.setGunId(chargingData.getGunId());//充电枪id
        //ChargingData中的价格就是当前的价格，因为充电可能会跨越多个计费阶段。  价格= 总度数/总费用。
        chargingProcessVO.setPowerFee(chargingData.getCurrentOneFee());//单位电价。
        chargingProcessVO.setTotalCapacity(chargingData.getTotalCapacity());//充电的总度数。

        Duration between = Duration.between(chargingData.getStartTime(), LocalDateTime.now());
        String totalTime=between.toDays() +"-" +  between.toHours() +"-" +  between.toMinutes() +"-"+between.toMillis();
        chargingProcessVO.setTotalTime(totalTime);

        return chargingProcessVO;
    }





}

